package at.jku.pervasive;

import java.awt.Color;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;

import at.jku.pervasive.filter.Filter;
import at.jku.pervasive.filter.MeanFilter;
import at.jku.pervasive.recognition.GestureCharacteristics;
import at.jku.pervasive.recognition.GestureListener;
import at.jku.pervasive.recognition.GesturePublisher;
import at.jku.pervasive.recognition.GestureRecognizer;
import at.jku.pervasive.recognition.GestureType;
import at.jku.pervasive.recognition.SocketGesturePublisher;

import com.google.common.collect.EnumHashBiMap;

public class Application implements PositionListener, GestureListener
{

	// singleton instance
	private static final Application INSTANCE = new Application();

	/**
	 * Get the application instance
	 * @return
	 */
	public static Application getInstance()
	{
		return INSTANCE;
	}

	// properties, read from config file
	private final Configuration config;
	private File saveFolder;
	private String host;
	private int port;
	private int numberOfTagPositionsForMean;

	// listeners
	private List<ApplicationListener> appListeners;
	private List<TagPositionListener> tagPositionListeners = new LinkedList<TagPositionListener>();
	private List<PositionListener> posListeners;
	private UbisenseListener listener;

	// needed for color management
	private final Color[] availableColors;
	private int currentColorIndex;

	// needed when a measurement is saved
	private DateFormat dateFormat;

	// gesture recognition and publication
	private final GestureRecognizer gestureRecognizer;
	private final GesturePublisher gesturePublisher;

	private final List<String> tracedTagIds;
	private Map<String, Color> tagIdColors = new TreeMap<String, Color>();
	private final EnumHashBiMap<TagAssignment, String> tagAssignmentMapping;
	private final Map<TagAssignment, LinkedList<Position>> recentTagPositions;
	private final Map<TagAssignment, Position> filteredTagPositions;

	private Application()
	{
		super();

		this.tagAssignmentMapping = EnumHashBiMap.create(TagAssignment.class);

		this.appListeners = new LinkedList<ApplicationListener>();
		this.posListeners = new LinkedList<PositionListener>();

		this.listener = new UbisenseListener();
		this.listener.addPositionListener(this);

		this.tracedTagIds = new LinkedList<String>();
		this.availableColors = new Color[] { Color.BLUE, Color.RED, Color.GREEN, Color.MAGENTA, Color.YELLOW };

		try
		{
			// instantiate configuration
			config = new PropertiesConfiguration("config.properties");
		} catch (ConfigurationException configEx)
		{
			throw new RuntimeException("exception while reading config file", configEx);
		}

		// get host and port, which shall be used for listening for positions
		this.host = config.getString("default.listenHost", "localhost");
		this.port = config.getInt("default.listenPort", 6666);

		// get the date format, which is used when a measurement is saved
		String dateFormatString = config.getString("default.dateFormat", "yyyyMMdd-HHmmss");
		this.dateFormat = new SimpleDateFormat(dateFormatString);

		// get the default save folder
		String defaultSaveFolderString = config.getString("default.saveFolder", ".");
		this.saveFolder = new File(defaultSaveFolderString);

		this.recentTagPositions = new TreeMap<TagAssignment, LinkedList<Position>>();
		this.filteredTagPositions = new TreeMap<TagAssignment, Position>();

		// get the number of tag positions, which shall be used for calculation
		// of mean
		this.numberOfTagPositionsForMean = config.getInt("default.numberOfTagPositionsForMean", 3);

		// get the default gesture characteristics
		GestureCharacteristics.ANGLE_TOLERANCE = config.getDouble("gesture.angleTreshold", 40.0D);
		GestureCharacteristics.EQUAL_HEIGHT_TOLERANCE = config.getDouble("gesture.equalHeightTreshold", 0.25D);
		GestureCharacteristics.LOWER_TOLERANCE = config.getDouble("gesture.lowerTreshold", 0.3D);

		// create a gesture recognizer and set default values
		this.gestureRecognizer = new GestureRecognizer();
		this.gestureRecognizer.setGestureRecognitionTime(config.getLong("gesture.recognitionTime", 1000L));
		this.gestureRecognizer.setGestureRecognitionTreshold(config.getDouble("gesture.precentage", 0.95D));
		this.gestureRecognizer.setZOffset(config.getDouble("gesture.zOffset", 0.45D));

		// get the port, which will be used when gestures are published on the
		// network
		int publishPort = config.getInt("default.gesturePublisher.port", 6668);
		this.gesturePublisher = new SocketGesturePublisher(publishPort);
		try
		{
			this.gesturePublisher.startProducing();
		} catch (IOException e)
		{
			e.printStackTrace();
		}

		addTagPositionListener(gestureRecognizer);
		gestureRecognizer.addGestureListener(this);
	}

	public void addApplicationListener(ApplicationListener appListener)
	{
		this.appListeners.add(appListener);
	}

	public void addGestureListener(GestureListener gestureListener)
	{
		this.gestureRecognizer.addGestureListener(gestureListener);
	}

	public void addPositionListener(PositionListener posListener)
	{
		this.posListeners.add(posListener);
	}

	public void addTagPositionListener(TagPositionListener tagPositionListener)
	{
		this.tagPositionListeners.add(tagPositionListener);
	}

	public double getAngleTolerance()
	{
		return GestureCharacteristics.ANGLE_TOLERANCE;
	}

	public Color getColorForTagId(String tagId)
	{
		return tagIdColors.get(tagId);
	}

	public Color getColorForTagPosition(TagAssignment tagPosition)
	{
		if (TagAssignment.RIGHT == tagPosition)
			return Color.GREEN;

		if (TagAssignment.LEFT == tagPosition)
			return Color.RED;

		if (TagAssignment.FRONT == tagPosition)
			return Color.MAGENTA;

		if (TagAssignment.BACK == tagPosition)
			return Color.CYAN;

		String tagId = this.tagAssignmentMapping.get(tagPosition);
		return getColorForTagId(tagId);
	}

	public File getDefaultOpenMeasurementFolder()
	{
		return saveFolder;
	}

	public double getEqualToHeightTolerance()
	{
		return GestureCharacteristics.EQUAL_HEIGHT_TOLERANCE;
	}

	public long getGestureRecognitionTime()
	{
		return this.gestureRecognizer.getGestureRecognitionTime();
	}

	public double getGestureRecognitionTreshold()
	{
		return this.gestureRecognizer.getGestureRecognitionTreshold();
	}

	public String getHost()
	{
		return this.host;
	}

	public double getLowerDifferenceTolerance()
	{
		return GestureCharacteristics.LOWER_TOLERANCE;
	}

	public Color getNextColor()
	{
		return availableColors[currentColorIndex++ % availableColors.length];
	}

	public int getNumberOfTracingTagIds()
	{
		return this.tracedTagIds.size();
	}

	public int getPort()
	{
		return this.port;
	}

	public List<String> getTracedTagIds()
	{
		return tracedTagIds;
	}

	public double getZOffset()
	{
		return this.gestureRecognizer.getZOffset();
	}

	public boolean isTracingTag(String tagId)
	{
		return this.tracedTagIds.contains(tagId);
	}

	public Measurement loadMeasurement(File measurementFile) throws FileNotFoundException
	{
		return Helper.readMeasurementSeries(new FileInputStream(measurementFile));
	}

	/**
	 * Adds the position as recent tag postion.
	 * @param tagAssignment
	 * @param newPosition
	 * @return <code>true</code> if the filtered position could have been
	 *         calculated, <code>false</code> otherwise.
	 */
	private boolean addRecentTagPosition(TagAssignment tagAssignment, Position newPosition)
	{
		LinkedList<Position> recentTagPosList = this.recentTagPositions.get(tagAssignment);
		if (recentTagPosList == null)
		{
			recentTagPosList = new LinkedList<Position>();
			this.recentTagPositions.put(tagAssignment, recentTagPosList);
		}

		// add the new position as first position in the list and trim the list
		// so it fits the size
		recentTagPosList.addFirst(newPosition);
		if (recentTagPosList.size() > numberOfTagPositionsForMean)
		{
			// remove last position, so the list is trimmed to correct size
			recentTagPosList.removeLast();

			// calculate filtered position for this tag and store in map for
			// later use
			Position filtered = calculatePos(recentTagPosList);
			this.filteredTagPositions.put(tagAssignment, filtered);

			return true;
		}

		return false;
	}

	@Override
	public void positionReceived(Position newPosition)
	{
		// inform all position listeners about received position
		for (PositionListener positionListener : posListeners)
		{
			positionListener.positionReceived(newPosition);
		}

		// check if the id of the tag is assigned; if so, inform all tag
		// position listeners for an updated tag position
		String tagId = newPosition.getId();
		TagAssignment tagAssignment = tagAssignmentMapping.inverse().get(tagId);
		if (tagAssignment != null)
		{
			boolean filteredPositionAvalable = addRecentTagPosition(tagAssignment, newPosition);
			Position filteredPosition = this.filteredTagPositions.get(tagAssignment);
			for (TagPositionListener tagPositionListener : tagPositionListeners)
			{
				tagPositionListener.tagPositionChanged(tagAssignment, newPosition);
				if (filteredPositionAvalable)
				{
					tagPositionListener.filteredTagPositionChanged(tagAssignment, filteredPosition);
				}
			}
		}
	}

	public void removeApplicationListener(ApplicationListener appListener)
	{
		this.appListeners.remove(appListener);
	}

	public void removePositionListener(PositionListener posListener)
	{
		this.posListeners.remove(posListener);
	}

	public void saveMeasurement(Measurement measurement)
	{
		System.out.println("saving measurement \n" + measurement.toString());

		Date startDate = measurement.getStartDate();
		String fileName = dateFormat.format(startDate) + ".xml";
		File measurementFile = new File(saveFolder, fileName);
		Helper.writeMeasurement(measurement, measurementFile);
	}

	public void setAngleTolerance(double angleTolerance)
	{
		GestureCharacteristics.ANGLE_TOLERANCE = angleTolerance;
	}

	public void setColorForTagId(String tagId, Color color)
	{
		tagIdColors.put(tagId, color);
	}

	public void setEqualHeightTolerance(double equalToHeightTolerance)
	{
		GestureCharacteristics.EQUAL_HEIGHT_TOLERANCE = equalToHeightTolerance;
	}

	public void setGestureRecognitionTime(long gestureRecognitionTime)
	{
		this.gestureRecognizer.setGestureRecognitionTime(gestureRecognitionTime);
	}

	public void setGestureRecognitionTreshold(double gestureRecognitionTreshold)
	{
		this.gestureRecognizer.setGestureRecognitionTreshold(gestureRecognitionTreshold);
	}

	public void setHost(String host)
	{
		this.host = host;
	}

	public void setLowerDifferenceTolerance(double lowerDifferenceTolerance)
	{
		GestureCharacteristics.LOWER_TOLERANCE = lowerDifferenceTolerance;
	}

	public void setPort(int port)
	{
		this.port = port;
	}

	public void setZOffset(double zOffset)
	{
		this.gestureRecognizer.setZOffset(zOffset);
	}

	public void startedTracingTag(String id)
	{
		this.tracedTagIds.add(id);
		for (ApplicationListener appListener : appListeners)
		{
			appListener.startedTracingTag(id);
		}
	}

	public void startListening()
	{
		startListening(host, port);
	}

	public void startListening(String host, int port)
	{
		this.listener.startListening(host, port);

		for (ApplicationListener appListener : appListeners)
		{
			appListener.startedListening(host, port);
		}
	}

	public void stopListening()
	{
		this.listener.stopListening();
		for (String tagId : tracedTagIds)
		{
			stoppedTracingTag(tagId);
		}
		tracedTagIds.clear();

		for (ApplicationListener appListener : appListeners)
		{
			appListener.stoppedListening(host, port);
		}
	}

	public void stoppedTracingTag(String id)
	{
		for (ApplicationListener appListener : appListeners)
		{
			appListener.stoppedTracingTag(id);
		}
	}

	public void stopTracingTag(String tagId)
	{
		this.tracedTagIds.remove(tagId);
		stoppedTracingTag(tagId);
	}

	public void tagAssignmentChanged(TagAssignment tagPosition, String tagId)
	{
		tagAssignmentMapping.put(tagPosition, tagId);

		for (ApplicationListener appListener : appListeners)
		{
			appListener.tagAssignmentChanged(tagPosition, tagId);
		}
	}

	private Position calculatePos(LinkedList<Position> list)
	{
		Filter meanFilter = new MeanFilter();
		LinkedList<Position> meanApplied = meanFilter.applyFilter(list);

		return meanApplied.getFirst();
	}

	public TagAssignment getTagAssignment(String tagId)
	{
		return tagAssignmentMapping.inverse().get(tagId);
	}

	@Override
	public void gestureFinished(GestureType gestureType, long duration)
	{
		gesturePublisher.gestureFinished(gestureType, duration);
	}

	@Override
	public void gestureRecognized(GestureType gestureType, Date timestamp, GestureCharacteristics gestureCharacteristics)
	{
		// TODO Auto-generated method stub

	}

	@Override
	public void gestureStarted(GestureType gestureType, Date timestamp)
	{
		gesturePublisher.gestureStarted(gestureType, timestamp);
	}

	public boolean getShowBody()
	{
		return this.config.getBoolean("view.showBody");
	}

	public boolean getShowFilteredPositions()
	{
		return this.config.getBoolean("view.showFilteredPositions");
	}

	public boolean getShowRawPositions()
	{
		return this.config.getBoolean("view.showRawPositions");
	}
}
